/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.rest;

import java.util.Map;
import java.util.Set;

import org.apache.camel.Category;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
import org.apache.camel.ExchangePattern;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.spi.FactoryFinder;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RestApiConsumerFactory;
import org.apache.camel.spi.RestApiProcessorFactory;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.DefaultEndpoint;
import org.apache.camel.util.HostUtils;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Expose OpenAPI Specification of the REST services defined using Camel REST DSL.
 */
@UriEndpoint(firstVersion = "2.16.0", scheme = "rest-api", title = "REST API", syntax = "rest-api:path",
             remote = false, consumerOnly = true, category = { Category.CORE, Category.REST }, lenientProperties = true)
public class RestApiEndpoint extends DefaultEndpoint {

    public static final String[] DEFAULT_REST_API_CONSUMER_COMPONENTS
            = new String[] { "platform-http", "servlet", "jetty", "undertow", "netty-http" };

    public static final String DEFAULT_API_COMPONENT_NAME = "openapi";
    public static final String RESOURCE_PATH = "META-INF/services/org/apache/camel/restapi/";

    private static final Logger LOG = LoggerFactory.getLogger(RestApiEndpoint.class);

    @UriPath
    @Metadata(required = true)
    private String path;
    @UriParam
    private String consumerComponentName;
    @UriParam
    private String apiComponentName;

    private Map<String, Object> parameters;

    public RestApiEndpoint(String endpointUri, RestApiComponent component) {
        super(endpointUri, component);
        setExchangePattern(ExchangePattern.InOut);
    }

    @Override
    public boolean isRemote() {
        return false;
    }

    @Override
    public RestApiComponent getComponent() {
        return (RestApiComponent) super.getComponent();
    }

    public String getPath() {
        return path;
    }

    /**
     * The base path
     */
    public void setPath(String path) {
        this.path = path;
    }

    public String getConsumerComponentName() {
        return consumerComponentName;
    }

    /**
     * The Camel Rest component to use for the consumer REST transport, such as jetty, servlet, undertow. If no
     * component has been explicitly configured, then Camel will lookup if there is a Camel component that integrates
     * with the Rest DSL, or if a org.apache.camel.spi.RestConsumerFactory is registered in the registry. If either one
     * is found, then that is being used.
     */
    public void setConsumerComponentName(String consumerComponentName) {
        this.consumerComponentName = consumerComponentName;
    }

    public String getApiComponentName() {
        return apiComponentName;
    }

    /**
     * The Camel Rest API component to use for generating the API of the REST services, such as openapi.
     */
    public void setApiComponentName(String apiComponentName) {
        this.apiComponentName = apiComponentName;
    }

    public Map<String, Object> getParameters() {
        return parameters;
    }

    /**
     * Additional parameters to configure the consumer of the REST transport for this REST service
     */
    public void setParameters(Map<String, Object> parameters) {
        this.parameters = parameters;
    }

    @Override
    public Producer createProducer() throws Exception {
        RestApiProcessorFactory factory = null;

        RestConfiguration config = CamelContextHelper.getRestConfiguration(getCamelContext(), getConsumerComponentName());

        // lookup in registry
        Set<RestApiProcessorFactory> factories = getCamelContext().getRegistry().findByType(RestApiProcessorFactory.class);
        if (factories != null && factories.size() == 1) {
            factory = factories.iterator().next();
        }

        // lookup on classpath using factory finder to automatic find it (just add camel-openapi-java to classpath etc)
        if (factory == null) {
            String name = apiComponentName != null ? apiComponentName : config.getApiComponent();
            if (name == null) {
                name = DEFAULT_API_COMPONENT_NAME; //use openapi first
            }
            FactoryFinder finder = getCamelContext().getCamelContextExtension().getFactoryFinder(RESOURCE_PATH);
            factory = finder.newInstance(name, RestApiProcessorFactory.class).orElse(null);
        }

        if (factory == null) {
            String name = apiComponentName != null ? apiComponentName : config.getApiComponent();
            if (name == null) {
                name = "swagger"; //use swagger as fallback
            }
            FactoryFinder finder = getCamelContext().getCamelContextExtension().getFactoryFinder(RESOURCE_PATH);
            factory = finder.newInstance(name, RestApiProcessorFactory.class).orElse(null);
        }

        if (factory != null) {

            // if no explicit port/host configured, then use port from rest configuration
            String host = "";
            int port = 80;

            if (config.getApiHost() != null) {
                host = config.getApiHost();
            } else if (config.getHost() != null) {
                host = config.getHost();
            }
            int num = config.getPort();
            if (num > 0) {
                port = num;
            }

            // if no explicit hostname set then resolve the hostname
            if (ObjectHelper.isEmpty(host)) {
                if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
                    host = "0.0.0.0";
                } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
                    host = HostUtils.getLocalHostName();
                } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
                    host = HostUtils.getLocalIp();
                }

                // no host was configured so calculate a host to use
                // there should be no schema in the host (but only port)
                String targetHost = host + (port != 80 ? ":" + port : "");
                getParameters().put("host", targetHost);
            }

            // the base path should start with a leading slash
            String path = getPath();
            if (path != null && !path.startsWith("/")) {
                path = "/" + path;
            }

            Processor processor = factory.createApiProcessor(getCamelContext(), path, config, getParameters());
            return new RestApiProducer(this, processor);
        } else {
            throw new IllegalStateException(
                    "Cannot find RestApiProcessorFactory in Registry or classpath (such as the camel-openapi-java component)");
        }
    }

    @Override
    public Consumer createConsumer(Processor processor) throws Exception {
        RestApiConsumerFactory factory = null;
        String cname = null;

        // we use the rest component as the HTTP consumer to service the API
        // the API then uses the api component (eg usually camel-openapi-java) to build the API
        if (getConsumerComponentName() != null) {
            Object comp = getCamelContext().getRegistry().lookupByName(getConsumerComponentName());
            if (comp instanceof RestApiConsumerFactory restApiConsumerFactory) {
                factory = restApiConsumerFactory;
            } else {
                comp = getCamelContext().getComponent(getConsumerComponentName());
                if (comp instanceof RestApiConsumerFactory restApiConsumerFactory) {
                    factory = restApiConsumerFactory;
                }
            }

            if (factory == null) {
                if (comp != null) {
                    throw new IllegalArgumentException(
                            "Component " + getConsumerComponentName() + " is not a RestApiConsumerFactory");
                } else {
                    throw new NoSuchBeanException(getConsumerComponentName(), RestApiConsumerFactory.class.getName());
                }
            }
            cname = getConsumerComponentName();
        }

        // try all components
        if (factory == null) {
            for (String name : getCamelContext().getComponentNames()) {
                Component comp = getCamelContext().getComponent(name);
                if (comp instanceof RestApiConsumerFactory restApiConsumerFactory) {
                    factory = restApiConsumerFactory;
                    cname = name;
                    break;
                }
            }
        }

        // lookup in registry
        if (factory == null) {
            Set<RestApiConsumerFactory> factories = getCamelContext().getRegistry().findByType(RestApiConsumerFactory.class);
            if (factories != null && factories.size() == 1) {
                factory = factories.iterator().next();
            }
        }
        // no explicit factory found then try to see if we can find any of the default rest consumer components
        if (factory == null) {
            RestApiConsumerFactory found = null;
            String foundName = null;
            for (String name : DEFAULT_REST_API_CONSUMER_COMPONENTS) {
                Object comp = getCamelContext().getComponent(name, true);
                if (comp instanceof RestApiConsumerFactory restApiConsumerFactory) {
                    found = restApiConsumerFactory;
                    foundName = name;
                    break;
                }
            }
            if (found != null) {
                LOG.debug("Auto discovered {} as RestApiConsumerFactory", foundName);
                factory = found;
            }
        }

        if (factory != null) {
            // calculate the url to the rest API service
            RestConfiguration config = CamelContextHelper.getRestConfiguration(getCamelContext(), cname);

            // calculate the url to the rest API service
            String path = getPath();
            if (path != null && !path.startsWith("/")) {
                path = "/" + path;
            }

            Consumer consumer = factory.createApiConsumer(getCamelContext(), processor, path, config, getParameters());
            configureConsumer(consumer);

            return consumer;
        } else {
            throw new IllegalStateException("Cannot find RestApiConsumerFactory in Registry or as a Component to use");
        }
    }

    @Override
    public boolean isLenientProperties() {
        return true;
    }
}
